package jpb.process;

import jpb.Assert;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

import static jpb.Assert.notNull;

/**
 * Collects output from executing processes and kills them by given timeout.
 *
 * @since v1.1
 */
public class ProcessExecutor {

    private final ExecutorService executorService;


    public ProcessExecutor(ExecutorService executorService) {
        this.executorService = executorService;
    }

    public ProcessExecutor() {
        this(Executors.newCachedThreadPool());
    }

    /**
     * Return future that will wait for <tt>process</tt> to finish and provide its return code and output.
     *
     * @param process process to wait for
     * @return future representing result of the process
     */
    public Future<ProcessResult> execute(Process process) {
        final ProcessOutputDrainer drainer = new ProcessOutputDrainer(process);
        final Future<String> output = executorService.submit(drainer);
        final Future<Integer> resultCode = executorService.submit(new ProcessWaiter(process, drainer));
        return new ProcessResultFuture(resultCode, output);
    }

    private static final class ProcessResultFuture implements Future<ProcessResult> {

        private final Future<Integer> resultCode;
        private final Future<String> output;

        private final AtomicReference<ProcessResult> result = new AtomicReference<ProcessResult>();

        public ProcessResultFuture(Future<Integer> resultCode, Future<String> output) {
            this.resultCode = resultCode;
            this.output = output;
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return resultCode.cancel(mayInterruptIfRunning) || output.cancel(mayInterruptIfRunning);
        }

        @Override
        public boolean isCancelled() {
            return resultCode.isCancelled() || output.isCancelled();
        }

        @Override
        public boolean isDone() {
            return result.get() != null;
        }

        @Override
        public ProcessResult get() throws InterruptedException, ExecutionException {
            if (isCancelled()) {
                return null;
            }
            if (isDone())
            {
                return result.get();
            }
            return doIt();
        }

        @Override
        public ProcessResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            if (isCancelled()) {
                return null;
            }
            if (isDone())
            {
                return result.get();
            }
            return doIt(timeout, unit);
        }

        private ProcessResult doIt() throws InterruptedException, ExecutionException {
            final Integer processResultCode = notNull("resultCode", resultCode.get());
            final String processOutput = notNull("output", output.get());
            result.compareAndSet(null, new ProcessResult(processResultCode, processOutput));
            return result.get();
        }

        private ProcessResult doIt(long timeout, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException {
            final Integer processResultCode = notNull("resultCode", resultCode.get(timeout, timeUnit));
            final String processOutput = notNull("output", output.get(timeout, timeUnit));
            result.compareAndSet(null, new ProcessResult(processResultCode, processOutput));
            return result.get();
        }
    }
}
